/************************************************************************
 *  Copyright (c) 2011-2012 HONG LEIMING.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to permit
 * persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 ***************************************************************************/
package net.zbus.rpc;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import net.zbus.ServiceHandler;
import net.zbus.ZBusException;
import net.zbus.ZMsg;

import org.apache.log4j.Logger;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;

class MethodInstance{
	public Method method;
	public Object instance;
	
	public MethodInstance(Method method, Object instance){
		this.method = method;
		this.instance = instance;
	}
}

public class RpcServiceHandler implements ServiceHandler {
	private static final Logger logger = Logger.getLogger(RpcServiceHandler.class);
	
	private static final String DEFAULT_ENCODING = "UTF-8"; 
	private Map<String,MethodInstance> methods = new HashMap<String, MethodInstance>();
	
	private boolean zbusLog = true; 
	
	
	public RpcServiceHandler(Object... services){ 
		addModule("", services);
	}
	
	public void addModule(String module, Object... services){
		for(Object service: services){
			this.initCommandTable(module, service);
		}
	}
	
	private void initCommandTable(String module, Object service){
		Class<?>[] classes = new Class<?>[service.getClass().getInterfaces().length+1];
		classes[0] = service.getClass(); 
		for(int i=1; i<classes.length; i++){
			classes[i] = service.getClass().getInterfaces()[i-1];
		}
		try { 
			for(Class<?> clazz : classes){
				Method [] methods = clazz.getMethods(); 
				for (Method m : methods) { 
					Remote cmd = m.getAnnotation(Remote.class);
					if(cmd != null){
						String method = cmd.id();
						if("".equals(method)){
							method = m.getName();
						}  
						String key = module + ":" + method;
						if(this.methods.containsKey(key)){
							logger.debug(module + "." + method + " duplicated"); 
						} else {
							logger.debug("register " + module + "." + method);
						}
						m.setAccessible(true);
						this.methods.put(key, new MethodInstance(m, service));  
					}
				} 
			}
		} catch (SecurityException e) {
			e.printStackTrace();
		}   
	}
	
	public byte[] handleJsonRequest(byte[] jsonData, String charsetName){
		Throwable error = null;
		Object result = null; 
		
		JSONObject req = null;
		String module = "";
		String method = null;
		JSONArray args = null;
		MethodInstance target = null;
		String debugMsgid = null ;
		String client = null;
		try{  
			String jsonStr = new String(jsonData, charsetName);
			req = (JSONObject) JSON.parse(jsonStr);
		}catch (Exception e) {
			e.printStackTrace();
			error = e;
		}
		
		if(error == null){
			try{ 
				module = req.getString("module");
				method = req.getString("method");
				args = req.getJSONArray("params");
                client = req.getString(Rpc.LANG_KEY);
                debugMsgid = req.getString("debugMsgid");
			}catch (Exception e) {
				error = e;
			}
			if(module == null){
				module = "";
			}
			if(method == null){
				error = new ZBusException("missing method name");
			}
		}
		
		if(error == null){
			String key = module+":"+method;
			if(this.methods.containsKey(key)){
				target = this.methods.get(key); 
			}else{
				if("".equals(module)){
					error = new ZBusException("method:'"+method + "' not found");
				} else {
					String msg = String.format("%s.%s not found", module, method);
					error = new ZBusException(msg);
				}
			}
		}
		
		if(error == null){
			try { 
				Class<?>[] types = target.method.getParameterTypes();
				if(types.length == args.size()){
					Object[] params = new Object[types.length]; 
					for(int i=0;  i<types.length; i++){
						params[i] = args.getObject(i, types[i]);
					}
					result = target.method.invoke(target.instance, params); 
				}else{
					error = new ZBusException("number of argument not match");
				} 
			}catch (Throwable e) {
			    e.printStackTrace();
				error = e;
			}
		} 
		JSONObject data = new JSONObject(); 
		if(error == null){    
			data.put("result", result); 
		} else {   
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			if(error.getCause() != null){
				error = error.getCause();
			}
			error.printStackTrace(pw); 
			data.put("error", error); 
			data.put("stack_trace", sw.toString());  
		}
		data.put("debugMsgid", debugMsgid);
		//support null value
		byte[] resBytes = null;
		if (Rpc.LANG_JAVA.equals(client)) {
			resBytes = Rpc.toJSONBytes(data, charsetName, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteClassName);
		} else {
			resBytes = Rpc.toJSONBytes(data, charsetName, SerializerFeature.WriteMapNullValue);
		}   
		return resBytes;
	}

	@Override
	public ZMsg handleRequest(ZMsg request) {  
		try { 
			if(request.frameSize() < 1){
				ZMsg reply = new ZMsg();
				reply.pushBack("400");
				reply.pushBack("request format error: support at least one frame");
				return reply;
			}
			
			byte[] jsonBytes = request.popFront();
			String encoding = DEFAULT_ENCODING;
			if(request.frameSize()>0){
				encoding = request.popFrontStr();
			}
			
			long start = System.currentTimeMillis();
			String logMsg = ""; 
			if(this.zbusLog){ 
				logMsg += "[ZBusReq]: "+ new String(jsonBytes, encoding);
				logger.debug(logMsg);
			}
			
			byte[] replyBytes = this.handleJsonRequest(jsonBytes, encoding);
			
			ZMsg reply = new ZMsg();
			reply.pushBack("200"); 
			reply.pushBack(replyBytes);
			
			long end = System.currentTimeMillis();
			if(this.zbusLog){ 
				String jsonStr = new String(replyBytes, encoding);
				logMsg += "\n      [ZBusRep]: Cost="+ (end-start) + "(ms), "+ jsonStr ;
				logger.debug(logMsg);
			}
			return reply;
			
		} catch (Throwable e) {
			ZMsg reply = new ZMsg();
			reply.pushBack("500");
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			reply.pushBack(sw.toString());
			return reply;
		} 
	}

	public boolean isZbusLog() {
		return zbusLog;
	}

	public void setZbusLog(boolean zbusLog) {
		this.zbusLog = zbusLog;
	} 
	 
}
